﻿#region License
/*Copyright (c) 2010 Andrew Collard (andrewcollard@hotmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.*/
#endregion

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

namespace TorchView.Windows
{
  /// <summary>
  /// Provides access to global mouse events
  /// </summary>
  public static class Mouse
  {
    #region Constants
    //Declare the mouse hook constant.
    //For other hook types, you can obtain these values from Winuser.h in the Microsoft SDK.
    private const int WH_MOUSE_LL = 14;
    #endregion

    #region Internal Fields
    private static int _hHook;
    /// <summary>
    /// Counter of the number of people who are listening to the mouse hook, when this gets to 0 the hook is removed
    /// </summary>
    private static int _HookCount;
    /// <summary>
    /// Used for locking
    /// </summary>
    private static object _SyncRoot = new object();
    private static HookProc _HookProc = new HookProc(HandleMouse);
    private static Thread _MessageThread;
    #endregion

    #region Internal Types
    private delegate int HookProc(int nCode, IntPtr wParam, IntPtr lParam);

    private enum MouseMessage
    {
      WM_MOUSEMOVE = 0x0200,

      WM_LBUTTONDOWN = 0x0201,
      WM_LBUTTONUP = 0x0202,

      WM_RBUTTONDOWN = 0x0204,
      WM_RBUTTONUP = 0x0205,

      WM_MOUSEWHEEL = 0x020A,
    }

    //Declare the wrapper managed MouseHookStructEx class.
    [StructLayout(LayoutKind.Sequential)]
    private class MouseHookStructEx
    {
      public MouseHookStruct MouseHookStruct;
      public short mouseData;
    }

    //Declare the wrapper managed MouseHookStruct class.
    [StructLayout(LayoutKind.Sequential)]
    private class MouseHookStruct
    {
      public POINT pt;
      public int hwnd;
      public int wHitTestCode;
      public int dwExtraInfo;
    }

    /// <summary>
    /// Used to notify when a subscribtion is no longer needed
    /// </summary>
    private class Unhooker : IDisposable
    {
      private IDisposable _Disposable;

      public Unhooker(IDisposable disposable)
      {
        _Disposable = disposable;
        EnsureHook();
      }

      public void Dispose()
      {
        _Disposable.Dispose();
        RemoveHook();
      }
    }

    /// <summary>
    /// A observable wrapper that provides a custom IDisposable
    /// </summary>
    private class ObservableWrapper<T> : IObservable<T>
    {
      public ISubject<T> Subject { get; private set; }
      public ObservableWrapper(ISubject<T> observable)
      {
        Subject = observable;
      }

      public IDisposable Subscribe(IObserver<T> observer)
      {
        return new Unhooker(Subject.Subscribe(observer));
      }
    }

    /// <summary>
    /// A mouse position
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    private class POINT
    {
      public int X;
      public int Y;

      public Point ToPoint()
      {
        return new Point(X, Y);
      }
    }
    #endregion

    #region Internal Methods

    //This is the Import for the SetWindowsHookEx function.
    //Use this function to install a thread-specific hook.
    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    private static extern int SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hInstance, int threadId);

    //This is the Import for the UnhookWindowsHookEx function.
    //Call this function to uninstall the hook.
    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    private static extern bool UnhookWindowsHookEx(int idHook);

    //This is the Import for the CallNextHookEx function.
    //Use this function to pass the hook information to the next hook procedure in chain.
    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    private static extern int CallNextHookEx(int idHook, int nCode, IntPtr wParam, IntPtr lParam);

    [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    private static extern IntPtr GetModuleHandle(string lpModuleName);

    private static void EnsureHook()
    {
      if (Monitor.TryEnter(_SyncRoot, 1000))
      {
        try
        {
          _HookCount++;
          if (_hHook == 0)
          {
            _MessageThread = new Thread(RunThread);
            _MessageThread.Name = "MouseListenerThread";
            _MessageThread.IsBackground = true;
            var resetEvent = new ManualResetEventSlim(false);
            _MessageThread.Start(resetEvent);
            resetEvent.Wait();
          }
        }
        finally
        {
          Monitor.Exit(_SyncRoot);
        }
      }
    }

    private static void RunThread(object param)
    {
      ManualResetEventSlim waitHandle = param as ManualResetEventSlim;
      using (Process curProcess = Process.GetCurrentProcess())
      using (ProcessModule curModule = curProcess.MainModule)
      {
        _hHook = SetWindowsHookEx(WH_MOUSE_LL, _HookProc, GetModuleHandle(curModule.ModuleName), 0);
      }
      waitHandle.Set();
      Dispatcher.Run();
    }

    private static void ShutdownThread()
    {
      UnhookWindowsHookEx(_hHook);
      _hHook = 0;
    }

    private static void RemoveHook()
    {
      if (Monitor.TryEnter(_SyncRoot, 100))
      {
        try
        {
          _HookCount--;
          if (_hHook != 0 && _HookCount <= 0)
          {
            var dispatcher = Dispatcher.FromThread(_MessageThread);
            dispatcher.Invoke((Action)ShutdownThread);
            dispatcher.InvokeShutdown();
          }
        }
        finally
        {
          Monitor.Exit(_SyncRoot);
        }
      }
    }
    #endregion

    #region Observables
    private static ObservableWrapper<Point> _MouseMoveObservable = new ObservableWrapper<Point>(new Subject<Point>());
    private static ObservableWrapper<Point> _MouseLeftButtonDownObservable = new ObservableWrapper<Point>(new Subject<Point>());
    private static ObservableWrapper<Point> _MouseLeftButtonUpObservable = new ObservableWrapper<Point>(new Subject<Point>());
    private static ObservableWrapper<Point> _MouseRightButtonDownObservable = new ObservableWrapper<Point>(new Subject<Point>());
    private static ObservableWrapper<Point> _MouseRightButtonUpObservable = new ObservableWrapper<Point>(new Subject<Point>());

    /// <summary>
    /// Get the MouseMove observable
    /// </summary>
    public static IObservable<Point> MouseMove { get { return _MouseMoveObservable.ObserveOnDispatcher().AsObservable(); } }

    /// <summary>
    /// Get the MouseLeftButtonDown observable
    /// </summary>
    public static IObservable<Point> MouseLeftButtonDown { get { return _MouseLeftButtonDownObservable.ObserveOnDispatcher().AsObservable(); } }

    /// <summary>
    /// Get the MouseLeftButtonUp observable
    /// </summary>
    public static IObservable<Point> MouseLeftButtonUp { get { return _MouseLeftButtonUpObservable.ObserveOnDispatcher().AsObservable(); } }

    /// <summary>
    /// Get the MouseRightButtonDown observable
    /// </summary>
    public static IObservable<Point> MouseRightButtonDown { get { return _MouseRightButtonDownObservable.ObserveOnDispatcher().AsObservable(); } }

    /// <summary>
    /// Get the MouseRightButtonUp observable
    /// </summary>
    public static IObservable<Point> MouseRightButtonUp { get { return _MouseRightButtonUpObservable.ObserveOnDispatcher().AsObservable(); } }

    #endregion

    #region Handle Mouse Callback
    private static int HandleMouse(int nCode, IntPtr wParam, IntPtr lParam)
    {
      //Marshall the data from the callback.
      MouseHookStruct data = (MouseHookStruct)Marshal.PtrToStructure(lParam, typeof(MouseHookStruct));
      var mouseMessage = (MouseMessage)wParam;
      if (nCode >= 0)
        NotifyListeners(data, mouseMessage);

      return CallNextHookEx(0, nCode, wParam, lParam);
    }

    private static void NotifyListeners(MouseHookStruct data, MouseMessage message)
    {
      switch (message)
      {
        case MouseMessage.WM_MOUSEMOVE:
          _MouseMoveObservable.Subject.OnNext(data.pt.ToPoint());
          break;
        case MouseMessage.WM_LBUTTONDOWN:
          _MouseLeftButtonDownObservable.Subject.OnNext(data.pt.ToPoint());
          break;
        case MouseMessage.WM_LBUTTONUP:
          _MouseLeftButtonUpObservable.Subject.OnNext(data.pt.ToPoint());
          break;
        case MouseMessage.WM_RBUTTONDOWN:
          _MouseRightButtonDownObservable.Subject.OnNext(data.pt.ToPoint());
          break;
        case MouseMessage.WM_RBUTTONUP:
          _MouseRightButtonUpObservable.Subject.OnNext(data.pt.ToPoint());
          break;
        default:
          break;
      }
    }
    #endregion
  }
}
